﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Audio;
using FarseerGames.FarseerPhysics;
using FarseerGames.FarseerPhysics.Dynamics;
using FarseerGames.FarseerPhysics.Controllers;
using FarseerGames.FarseerPhysics.Dynamics.Springs;
using FarseerGames.FarseerPhysics.Collisions;
using FarseerGames.FarseerPhysics.Factories;
using GrapplingHookGameData;

namespace GrapplingHookGame
{
    /// <summary>
    /// Singleton class representing a Game Session. Holds all game and state information
    /// </summary>
    class GameSession:IGameDataCallbacks
    {
        #region Singleton

        /// <summary>
        /// The single Session instance that can be active at a time.
        /// </summary>
        private static GameSession singleton;
        #endregion

        #region Variables

        //"components of this game"
        public static ScreenManager ScreenManager { get { return (singleton == null ? null : singleton.screenManager); } }
        private ScreenManager screenManager;
        public static NetworkSession NetworkSession { get { return (singleton == null ? null : singleton.networkSession); } }
        private NetworkSession networkSession;
        public static SoundComponent SoundComponent { get; private set; }
        public static Level CurrentLevel { get; private set; }
        public static PhysicsSimulator PhysicsSimulator { get; private set; }
        public static ContentManager ContentManager { get; private set; }

        //public game related variables
        public int Rounds { get; private set; }
        public int CurrentRound { get; private set; }

        //private game related variables
        private static List<GameObject> gameObjects;
        private static ExplosionController grenadeExplotionController;
        public static ExplosionParticleSystem ExplosionParticleSystem { get { return (singleton == null ? null : singleton.explosionParticleSystem); } }
        private ExplosionParticleSystem explosionParticleSystem;

        private static PacketWriter packetWriter;
        private static PacketReader packetReader;

        private enum GameState { IN_PROGRESS, OVER }
        private static GameState currentState = GameState.IN_PROGRESS;

        private GameplayScreen gameplayScreen;
        #endregion

        #region Initialization

        /// <summary>
        /// Private constructor of a Session object.
        /// </summary>
        /// <remarks>
        /// The lack of public constructors forces the singleton model.
        /// </remarks>
        private GameSession(ScreenManager screenManager, GameplayScreen gameplayScreen, NetworkSession networkSession)
        {
            this.screenManager = screenManager;
            this.gameplayScreen = gameplayScreen;
            this.networkSession = networkSession;
            ContentManager = screenManager.Game.Content;

            packetWriter = new PacketWriter();
            packetReader = new PacketReader();
            gameObjects = new List<GameObject>();

            CurrentLevel = new Level();

            // create the particle system
            explosionParticleSystem = new ExplosionParticleSystem(10);
            explosionParticleSystem.Initialize();

        }
        /// <summary>
        /// Start a new session based on the data provided. Usually called from LoadContent.
        /// </summary>
        public static void StartNewSession(ScreenManager screenManager, GameplayScreen gameplayScreen, NetworkSession networkSession, string levelName, int rounds)
        {
            // end any existing session
            EndSession();

            // create a new singleton
            singleton = new GameSession(screenManager, gameplayScreen, networkSession);
            singleton.Rounds = rounds;
            singleton.CurrentRound = 0; //will be set to 1 by first call to StartNewRound()

            PhysicsSimulator = new PhysicsSimulator(new Vector2(0, Constants.GRAVITY));

            // set up the initial level
            CurrentLevel.LoadContent(levelName, ContentManager, ScreenManager.GraphicsDevice, PhysicsSimulator);

            // set up the initial players
            int xpos = 0;
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                Player p = new Player(singleton, new Vector2(xpos += 100, 200));
                p.LoadContent(ScreenManager.GraphicsDevice, ContentManager, GameSession.PhysicsSimulator);
                gamer.Tag = p;
            }

            //set up collision controller for explosions
            grenadeExplotionController = new ExplosionController(PhysicsSimulator);
            grenadeExplotionController.CollisionGroup = 2;
            grenadeExplotionController.OnExplotion += singleton.ExplotionCallback;

            singleton.explosionParticleSystem.LoadContent(ContentManager);


            //start music
            // Search through Game.Components to find the SoundComponent.
            foreach (IGameComponent component in ScreenManager.Game.Components)
            {
                SoundComponent soundComponent = component as SoundComponent;
                if (soundComponent != null)
                {
                    SoundComponent = soundComponent;
                    SoundComponent.PlayBackgroundMusic("InGameMusic");
                    break;
                }
            }
            singleton.StartNewRound();
        }

        //Starts a new round
        public void StartNewRound()
        {
            if (CurrentRound <= Rounds)
            {
                foreach (NetworkGamer gamer in networkSession.AllGamers)
                {
                    Player p = (Player)gamer.Tag;
                    p.health = Constants.PLAYER_HEALTH;
                }
                CurrentRound++;
                currentState = GameState.IN_PROGRESS;
            }
        }

        #endregion

        #region Updating


        /// <summary>
        /// Update the session for this frame.
        /// </summary>
        /// <remarks>This should only be called if there are no menus in use.</remarks>
        public static void Update(GameTime gameTime)
        {
            //Call correct update methos based on if we are host or not
            if (NetworkSession.IsHost)
            {
                UpdateServer(gameTime);
            }
            //else if client, read positions from server
            else
            {
                UpdateClient(gameTime);
            }

            //update particles
            singleton.explosionParticleSystem.Update(gameTime);

            //check if round is over
            string winner;
            if ( currentState != GameState.OVER && IsGameOver(out winner))
            {
                currentState = GameState.OVER;
                ScreenManager.AddScreen(new GameOverScreen(NetworkSession, singleton, winner));
            }
        }

        #region Updating Server
        /// <summary>
        /// The server's update method.
        /// Read input from players.
        /// Run physics simulation and send all positions to clients.
        /// </summary>
        /// <param name="gameTime"></param>
        private static void UpdateServer(GameTime gameTime)
        {
            //read player positions from client
            ReadDataFromClient();

            //update players
            foreach (NetworkGamer gamer in NetworkSession.AllGamers)
            {
                Player p = gamer.Tag as Player;
                p.Update(gameTime, true);
                p.playerStanding = false;
            }

            //update physics simulator
            GameSession.PhysicsSimulator.Update(gameTime.ElapsedGameTime.Milliseconds / 1000.0f);

            //update game objects
            for (int i = 0; i< gameObjects.Count; i++)
            {
                //update all game objects
                gameObjects[i].Update(gameTime, true);
            }


            SendDataToClients();



        }

        /// <summary>
        /// Read incooming network packets from clients and update player
        /// </summary>
        private static void ReadDataFromClient()
        {
            // Read any incoming network packets.
            foreach (LocalNetworkGamer gamer in NetworkSession.LocalGamers)
            {
                // Keep reading as long as incoming packets are available.
                while (gamer.IsHost && gamer.IsDataAvailable)
                {
                    NetworkGamer sender;

                    // Read a single packet from the network.
                    gamer.ReceiveData(packetReader, out sender);

                    if (!sender.IsLocal)
                    {
                        // Look up the Player associated with whoever sent this packet.
                        Player p = sender.Tag as Player;
                        p.ReadDataFromClient(packetReader);
                    }
                }
            }

        }

        /// <summary>
        /// Send information needed by clients to them
        /// </summary>
        private static void SendDataToClients()
        {
            // First off, our packet will indicate how many player it has data for.
            packetWriter.Write(NetworkSession.AllGamers.Count);
            foreach (NetworkGamer gamer in NetworkSession.AllGamers)
            {
                Player p = gamer.Tag as Player;

                // Write the state of players into the output network packet.
                p.WriteDataToClients(packetWriter);
            }


            // Send the combined data for all players to everyone in the session.
            LocalNetworkGamer server = (LocalNetworkGamer)NetworkSession.Host;
            server.SendData(packetWriter, SendDataOptions.InOrder);
        }
        #endregion

        #region Update Client
        /// <summary>
        /// The clients' update method, only runs on clients (not server).
        /// Send player actions to server
        /// Read positions from network.
        /// </summary>
        /// <param name="gameTime"></param>
        private static void UpdateClient(GameTime gameTime)
        {
            foreach (LocalNetworkGamer gamer in NetworkSession.LocalGamers)
            {
                //SEND ACTIONS
                Player p = gamer.Tag as Player;
                p.WriteDataToHost(packetWriter);

                // Send our input data to the server.
                gamer.SendData(packetWriter, SendDataOptions.InOrder, NetworkSession.Host);


                //READ Positions
                //Keep reading as long as incoming packets are available.
                while (gamer.IsDataAvailable)
                {
                    NetworkGamer sender;

                    // Read a single packet from the network.
                    gamer.ReceiveData(packetReader, out sender);

                    // If a player has recently joined or left, it is possible the server
                    // might have sent information about a different number of players
                    // than the client currently knows about. If so, we will be unable
                    // to match up which data refers to which player. The solution is
                    // just to ignore the packet for now: this situation will resolve
                    // itself as soon as the client gets the join/leave notification.
                    if (NetworkSession.AllGamers.Count != packetReader.ReadInt32())
                        continue;

                    // This packet contains data about all the players in the session.
                    foreach (NetworkGamer remoteGamer in NetworkSession.AllGamers)
                    {
                        p = remoteGamer.Tag as Player;
                        // Read the state of this player from the network packet.
                        p.ReadDataFromHost(packetReader);
                        p.Update(gameTime, false);

                    }
                }
            }
        }
        #endregion
        
        public static void HandleInput(InputState input)
        {
            // Read inputs for all locally controlled player.
            foreach (LocalNetworkGamer gamer in NetworkSession.LocalGamers)
            {
                Player p = gamer.Tag as Player;
                KeyboardState playerKBState = input.CurrentKeyboardStates[(int)gamer.SignedInGamer.PlayerIndex];
                KeyboardState playerLastKBState = input.LastKeyboardStates[(int)gamer.SignedInGamer.PlayerIndex];
                GamePadState playerPadState = input.CurrentGamePadStates[(int)gamer.SignedInGamer.PlayerIndex];
                GamePadState playerLastPadState = input.LastGamePadStates[(int)gamer.SignedInGamer.PlayerIndex];

                if (playerKBState.IsKeyDown(Keys.Right) ||
                    playerPadState.DPad.Right == ButtonState.Pressed ||
                    playerPadState.ThumbSticks.Left.X > 0
                    )
                {
                    p.nextMoveAction = Player.PlayerMoveActions.RIGHT;
                }
                else if (playerKBState.IsKeyDown(Keys.Left) ||
                    playerPadState.DPad.Left == ButtonState.Pressed ||
                    playerPadState.ThumbSticks.Left.X < 0
                    )
                {
                    p.nextMoveAction = Player.PlayerMoveActions.LEFT;
                }

                if ((playerKBState.IsKeyDown(Keys.Down)  && playerLastKBState.IsKeyUp(Keys.Down)) ||
                    (playerPadState.Triggers.Left == 0 && playerLastPadState.Triggers.Left > 0)
                    )
                {
                    p.nextRopeAction = Player.PlayerRopeActions.DISCONNECT_ROPE;
                }

                if ((playerKBState.IsKeyDown(Keys.Up) && playerLastKBState.IsKeyUp(Keys.Up)) ||
                    (playerPadState.Triggers.Left > 0 && playerLastPadState.Triggers.Left == 0)
                    )
                {
                    p.nextRopeAction = Player.PlayerRopeActions.FIRE_ROPE;
                }

                //use X to throw grenades, this prevents accidental presses when entering the menu
                if ((playerKBState.IsKeyDown(Keys.RightControl) && playerLastKBState.IsKeyUp(Keys.RightControl)) ||
                    (playerPadState.Buttons.X == ButtonState.Pressed && playerLastPadState.Buttons.X == ButtonState.Released)
                    )
                {
                    p.nextGunAction = Player.PlayerWeaponActions.GRENADE;
                }

                //use Y to throw shurikens, can't throw both shurikens and grenades
                if ((playerKBState.IsKeyDown(Keys.RightShift) && playerLastKBState.IsKeyUp(Keys.RightShift)) ||
                    (playerPadState.Buttons.Y == ButtonState.Pressed && playerLastPadState.Buttons.Y == ButtonState.Released)
                    )
                {
                    p.nextGunAction = Player.PlayerWeaponActions.SHURIKEN;
                }
            }

        }


        #endregion

        #region Callbacks from game objects
        public void GrenadeExploded(Grenade grenade)
        {
            SoundComponent.SoundBank.PlayCue("Explosion");
            explosionParticleSystem.AddParticles(grenade.Body.Position);

            //only hosts adds explosion to physics engine
            if ( networkSession.IsHost)
            {
                grenadeExplotionController.AddExplotion(grenade.Body.Position, Constants.GRENADE_EFFECT_IMPULSE, Constants.GRENADE_EFFECT_DISTANCE, grenade);
            }
        }

        public void RopeConnected()
        {
            SoundComponent.SoundBank.PlayCue("Klack");
        }

        public bool onGrenadeCollision(Geom geometry1, Geom geometry2, ContactList contactList)
        {
            Grenade g = (Grenade)geometry1.Tag;
            //check for wall collision
            if (geometry2.Body.IsStatic)
            {
                if (g.CurrentState == Grenade.GrenadeState.TICKING && g.Body.LinearVelocity.Length() > Constants.SOUND_IDLE_VELOCITY)
                {
                    SoundComponent.SoundBank.PlayCue("Klack");
                }
            }
            return true;
        }


        /// <summary>
        /// Called when Shurikens collide with other object. Walls stop shurikens. 
        /// </summary>
        /// <param name="geometry1"></param>
        /// <param name="geometry2"></param>
        /// <param name="contactList"></param>
        /// <returns></returns>
        public bool onShurikenCollision(Geom geometry1, Geom geometry2, ContactList contactList)
        {
            Shuriken s = (Shuriken)geometry1.Tag;
            if( s.CurrentState != Shuriken.ShurikenState.FLYING ) return true;

            //walls stop shurikens
            if (geometry2.Body.IsStatic)
            {
                if (s.Body.LinearVelocity.Length() > Constants.SOUND_IDLE_VELOCITY)
                {
                    SoundComponent.SoundBank.PlayCue("Klack");
                }
                s.Extinguish();
            }
            //player loses health
            else if (geometry2.Tag is Player)
            {
                Player p = (Player)geometry2.Tag;
                p.health -= Constants.SHURIKEN_DAMAGE;
                s.Extinguish();
            }
            //grenades explode
            else if (geometry2.Tag is Grenade)
            {
                Grenade g = (Grenade)geometry2.Tag;
                g.ExplodeGrenade();
            }
            //rope is cut
            else if (geometry2.Tag is SpringRectangleRope)
            {
                SpringRectangleRope r = (SpringRectangleRope)geometry2.Tag;
                r.DisconnectRope();
            }
            return true;
        }

        /// <summary>
        /// Called for each physics object that is affected by an explotion
        /// </summary>
        /// <param name="source"></param>
        /// <param name="affectedObject"></param>
        private void ExplotionCallback(Object source, Geom affectedObject)
        {
            if (affectedObject.Tag != null)
            {
                if (affectedObject.Tag is Player)
                {
                    Player p = (Player)affectedObject.Tag;
                    p.health -= Constants.GRENADE_DAMAGE;
                }
                //ignite other grenades
                else if (affectedObject.Tag is Grenade)
                {
                    Grenade g = (Grenade)affectedObject.Tag;
                    g.Timer = Math.Min(g.Timer, Constants.GRENADE_IGNITE_DELAY_MS);
                }
            }
        }
        
        #endregion



        #region Drawing
        /// <summary>
        /// Draws the session environment to the screen
        /// </summary>
        public static void Draw(GameTime gameTime)
        {
            SpriteBatch spriteBatch = singleton.screenManager.SpriteBatch;
            CurrentLevel.Draw(spriteBatch);

            // For each person in the session...
            foreach (NetworkGamer gamer in NetworkSession.AllGamers)
            {
                // Look up the Player object belonging to this network gamer.
                Player p = gamer.Tag as Player;
                p.Draw(ScreenManager.SpriteBatch);
            }

            //Draw game objects
            foreach (GameObject obstacle in gameObjects)
            {
                obstacle.Draw(ScreenManager.SpriteBatch);
            }

        }


              #endregion



        private static bool IsGameOver(out string winner)
        {
            int livePlayers =  NetworkSession.AllGamers.Count;
       
            string potentialWinner = null;
            //find how many live players are left?
            foreach( NetworkGamer gamer in  NetworkSession.AllGamers)
            {
                Player p = (Player)gamer.Tag;
                if( p.health <= 0 ) livePlayers--;
                else potentialWinner = gamer.Gamertag;
            }

            //check if all players are dead
            if (livePlayers <= 0)
            {
                winner = Resources.NoWinner;
                return true;
            }
            else if (NetworkSession.AllGamers.Count > 1 && livePlayers == 1)
            {
                winner = potentialWinner;
                return true;
            }
            winner = null;
            return false;
        }



        /// <summary>
        /// End the current session.
        /// </summary>
        public static void EndSession()
        {
            // exit the gameplay screen
            if (singleton != null)
            {
                GameplayScreen gameplayScreen = singleton.gameplayScreen;
                singleton.gameplayScreen = null;


                // clear the singleton
                singleton = null;

                if (gameplayScreen != null)
                {
                    gameplayScreen.ExitScreen();
                }
            }
        }

    
    }
}
